はじめに
深層学習(Deep Learning、ディープ・ラーニング)は、とても簡単です1。簡単な理由は、以下の2つ。
- TensorFlowやChainerといったライブラリが抽象化してくれるため、詳細(難しい数式等)を理解していなくても使える。
- 汎用性が高いため、いわゆる統計処理や機械学習のような複数のアルゴリズムの使い分けが不要。必要な前提知識が少なくて済む。
もし嘘だと思うなら、「2. インストール」の記述に従ってTensorFlowをインストールして、「4. プログラムの作成」の深度センサーのデータからの一般物体認識プログラムを実行してみてください。簡単(数式はゼロ!)な短い(全部で185行!)プログラムなのに、深度センサーの前に置かれたものが何なのかを高い精度(80%以上!)で答えてくれます。
深層学習がどのように動作するのかは、「3. 深層学習の仕組み」で述べます。ここを読んで深層学習の仕組みを理解すれば、「4. プログラムの作成」のソース・コードを少し修正するだけで、すぐに深層学習を活用する新たなサービスを作成できます。
深層学習は本当に簡単で、しかも有用です。「よく分からないけれど難しそう」という理由で使わないのはあまりにもったいない。深層学習をじゃんじゃん使って、鼻歌交じりに人工知能してみませんか? かなり面白いですよ。
インストール
本章では、深層学習のライブラリの1つであるGoogleのTensorFlowと、TensorFlowの実行に必要なPythonをインストールします。
インストール先となるOSは、シェアが最も高いWindowsとしましょう。
Pythonのインストール
まずは、プログラミング言語のPythonをインストールします。科学技術コンピューティングといえばPythonですもんね。
Pythonのバージョンは、3.x系で
Pythonには2.x系と3.x系という互換性がない2つのバージョンがあって、だからどちらにしようか迷うことが多いようですけど、本稿では3.x系を選択します。
3.x系の良いところは、2.x系との互換性を捨て去ることで得た一貫性のある分かりやすい言語仕様です。そして悪いところは、広い世の中には2.x系にしか対応していないライブラリがあって、それらが使えないこと……なのですけど、本稿が使用するTensorFlowは、Windows環境では3.xにしか対応していないというまさかの逆パターンなんですよ。というわけで、無条件で3.xとなります。
Python 3.5.3のインストール
Webブラウザーでhttps://www.python.org/を開き、一番下までスクロールして、Downloadsの[Windows]リンクをクリックしてください。
2017年2月現在のTensorFlow(バージョンは0.12.1)は、Windows環境では3.5.xの64bit環境しかサポートしませんから、3.5.xの中で最新の[Python 3.5.3 - 2017-01-17]の下の[Windows x86-64 executable installer]をクリックして、ダウンロードしました。
ダウンロードした「python-3.5.3-amd64.exe」を実行して、[Install Now]をクリックしてください。
以下のウィンドウが表示されたら、インストールは完了です。
venvで、TensorFlow用の仮想環境を作る
素のPythonは実はできることが少なくて、だから多数のライブラリを追加してできることを増やしていきます。TensorFlowだってライブラリですもんね。
ただし、多数のライブラリをインストールすると、ライブラリ間で競合が発生してしまうかもしれません。そもそも、Pythonのバージョンだって複数必要になるかもしれませんしね(もしTensorFlowを使わないなら、最新のPythonの3.6.xを使ってみたいでしょ?)。この問題を避けるには、適当な単位(プロジェクト単位等)で仮想環境を作ればよい。Pythonの3.x系には仮想環境を作るためのvenvというツールが付属していますから、それを使用しましょう。
というわけで、コマンドプロンプトを起動して適当なディレクトリ(Documents等)に移動し、以下のコマンドを実行してください。
> %HOMEDRIVE%%HOMEPATH%\AppData\Local\Programs\Python\Python35\python -m venv TensorFlow上のコマンドを実行すると、TensorFlowというディレクトリが作成されます。このディレクトリが、Pythonの仮想環境になります。
仮想環境をアクティブにするには、作成したTensorFlowディレクトリに移動してscripts\activateを実行します。
> cd TensorFlow
> scripts\activate仮想環境がアクティブになると、プロンプトが(TensorFlow) >のように変わります。仮想環境がアクティブになれば、先ほどのように長いパスを入力しなくても、ただpythonと入力するだけでPythonのインタープリターが起動します。
そうそう、Pythonのインタープリターを終了したい場合は、exit()と入力してください。
TensorFlowのインストール
深層学習のライブラリとしては、2017年2月現在ならTensorFlowかChainerのどちらかを選んでおけば問題なく幸せになれます。本稿では、筆者の独断でTensorFlowとします。無理やり理由を挙げるなら、Google製なので寄らば大樹みたいな安心感があるところかなぁと2。
Microsoft Visual C++ 2015 Redistributable
前準備として、Visual C++ 2015 Redistritutableをインストールしておいてください。TensorFlowは、Visual C++ 2015 Redistritutableに含まれるMSVCP140.DLLを使用しています。
https://www.microsoft.com/en-us/download/details.aspx?id=53587からVisual C++ 2015 Redistritutableダウンロードして実行すれば、インストールは完了です。
pip
ライブラリのWebサイトを開いてインストール・プログラムをダウンロードしてインストール、そのライブラリが依存するライブラリについても漏れ無く同様の手順を実行しておいてね……なんてのはやってられないので、今時の言語にはたいていライブラリを管理するシステムが付属しています(Rubyならgem、Haskellならcabal)。Pythonの場合は、pipというコマンドでライブラリを管理するのが一般的です。TensorFlowも、このpipでインストールできます。
というわけで、Pythonのインストール時に作成した仮想環境をアクティブ化して、以下のコマンドを実行してください3。
(TensorFlow) > pip install tensorflowこれだけで、TensorFlowの実行に必要なnumpyなどの他のライブラリも含めて全部、一括でインストールされます。
これで終了。正しくインストールされたか、確認してみましょう。TensorFlowを使用して、1 + 1を計算してみます。
はい、1 + 1の計算結果である2が表示されました。深層学習する準備は、これで完璧ですね。
深層学習とは?
TensorFlowの環境ができたから、さっそくプログラミング……の前に、深層学習とはどんなものなのか、その概要を勉強することにしましょう。4章のコードをコピー&ペーストしてちょっとだけ修正するだけでもTensorFlowを使用した深層学習はできるのですけど、概要を理解していた方が、より良く深層学習できますもんね。
形式ニューロン
まずは、基礎から。深層学習はニューラル・ネットワークを学習させるテクニックなのですが、そのニューラル・ネットワークを構成する要素であるニューロンの話から始めさせてください。
生物のニューロンは、その細胞体から何本も生えた樹状突起で刺激を受け取って、なんだかよくわからない基準でいい感じに刺激群を判断して、軸索を通じて次のニューロンに刺激を送ったり送らなかったりします。こんなのがいっぱい集まって、私達の脳はできあがっているんだって。
***絵***
で、このニューロンの「なんだかよくわからない基準でいい感じ」の部分を、はるか昔の1943年にマッカロックさんとピッツさんが形式ニューロンとしてソフトウェア化してくれました。図にすると、以下のような感じ。
***絵***
Pythonのコードにすると、以下のような感じ。
weights = (5, 2, 2) # 重み
threshold = 3 # しきい値
# 形式ニューロン
def formal_neuron(*xs): # xsは3つの真偽値
# 真である場合の重みを足しあわせて、しきい値を超えたら真。そうでなければ偽。
return sum(weights[i] for i, x in enumerate(xs) if x) > thresholdこの形式ニューロン、こんな単純な構造のくせに、なかなかいい感じに判断してくれるんですよ。
具体的にいきましょう。先ほどのコードのformal_neuronに、私が今週末に遊びに行くかどうかを表現させてみます。引数であるxsを構成する3つの真偽値は、それぞれ「晴れている」、「読むべき本がない」、「やらなければならない仕事がない」とします(否定形でわかりづらくてごめんなさい。マイナスの数字を使うのは難しかったの……)。
私の趣味はオートバイに乗ることで、そして独身で家族いなくてコミュ症で友達いません。だから、晴れていれば(他の条件がどうであっても)オートバイでツーリングに行きます。確認してみましょう。
>>> formal_neuron(True, False, False) # 晴れ、本あり、仕事あり。
True # 遊びに行く
>>>私は、読むべき有益な本があっても仕事が残っていても、晴れていればオートバイで出かけちゃう阿呆だということがわかりました。光に反応する昆虫みたいですな。
でも、雨が降ったらどうなるのでしょうか? 続けて試してみましょう。
v>>> formal_neuron(False, True, False) # 雨、本なし、仕事あり。
False # 遊びに行かない。
>>> formal_neuron(False, False, True) # 雨、本あり、仕事なし。
False # 遊びに行かない。
>>> formal_neuron(False, True, True) # 雨、本なし、仕事なし。
True # 遊びに行く。
>>> なるほど、まだ読んでいない本があるならそれを読む、仕事があるなら仕事をする、どちらもないなら、雨であってもカッパを着てオートバイに乗るわけですね。こんな単純なコードなのに、いい感じに判断できていて面白いでしょ?
でも、もっと面白いことがあるんです。重みであるweightsとしきい値であるthresholdを調整するだけで、私とは異なる人の場合も表現できるんですよ。たとえば、しきい値を4にすれば(threshold = 4)、晴れていれば遊びに行くけど雨ならば何があっても遊びに行かないという、雨が嫌いなオートバイ大好き人間になります。晴れの重みを減らして本の重みを増やしたなら(weights = [2, 5, 2])、ビブリオマニアの読子=リードマンさんですね。
形式ニューロンは、ANDとOR、NOTなんてのも表現できます。形式ニューロンを多段に組み合わせて良いのであれば、XORも表現できます。
from itertools import chain
def and_(*xs):
# weightsとthresholdは、関数内に埋め込んでいます。
return sum((1, 1)[i] for i, x in enumerate(xs) if x) > 1.5
def or_(*xs):
return sum((1, 1)[i] for i, x in enumerate(xs) if x) > 0.5
def not_(*xs):
return sum((-1,)[i] for i, x in enumerate(xs) if x) > -0.5
def xor_(*xs):
def next_layer(*next_xs):
return sum((1, 1, -2)[i] for i, x in enumerate(next_xs) if x) > 0.5
return next_layer(*chain(xs, ((sum((1, 1)[i] for i, x in enumerate(xs) if x) > 1.5),)))スゲー。ANDとORとNOTとXORを表現できるってことは、つまり、どんな電子回路でも作れるってことです。形式ニューロンを集めれば、CPUを作れちゃうんですよ。
これなら、もっとスゴイこともできそうです。適切な重みとしきい値を持った形式ニューロンをいい感じに積み重ねていけば、生物の脳と同様の判断、たとえば網膜に映った画像から餌の有無を判断できるかもしれません。だって、形式ニューロンは生物のニューロンを模しているんですから!
単純パーセプトロン
……ごめんなさい。そうは問屋が卸してくれませんでした。だって、パラメーター設定が難しすぎますもん。前節のXORのコードを書けたのは、Wikipediaの形式ニューロンの説明の中にパラメーターが載っていたからで、私が頑張ってパラメーターを設定したからではありません。XOR程度でこれなのですから、画像認識のためのパラメーター調整なんてのは、人間がやれる作業じゃあないですよ。
では、どうするか? 人間ができないなら、機械にやらせてしまえばよいわけです。そのために、1957年にローゼンブラットさんがパーセプトロンを発明しました。
パーセプトロンをコードにすると、以下のようになります。形式ニューロンとの違いは、入力値の要素と重みの要素を掛け算するようになったのと、しきい値(threshold)がバイアス(bias)と改名されて比較演算子の左に移動した程度。あと、掛け算をするので、入力値は実数になるところ。なお、機械学習するんだからパラメーター調整は私がやらなくても良いので、以下のコードでは重みもバイアスも適当な値にしてあります。
weights = (0.0, 1.0) # 重み
bias = -0.5 # バイアス
def perceptron(*xs):
return 1 if sum(x * weight for x, weight in zip(xs, weights)) + bias >= 0 else 0パーセプトロンの良い所は、重みやバイアスを調整しやすいところです。
形式ニューロンだと、判断に間違えた時に、重みやバイアスをどの程度変更したらよいのかが分からないですよね。でも、パーセプトロンなら、x * weightsになっていますから、xに関係した数値でweightを増減させてあげればよさそうに思えます。biasについては、xが1だとみなしちゃえばよいでしょう。
方針が定まったので、このパーセプトロンを実際に機械学習させます。今回は、昭和の男の子がみんな大好きだったスーパー・カーのオートバイ版であるスーパー・スポーツ(とにかく速くて馬鹿っぽいオートバイだと思ってください)の判別とします。上のコードでは重みが2個なのでxsの数は2つ、オートバイを表現する数値2つということで、エンジンの大きさ(排気量)と価格を入力にします。
さっそく、日本のバイクメーカーであるホンダとヤマハとカワサキと私が愛するスズキのスーパー・スポーツなオートバイのデータを集めてきました。判断のためにはスーパー・スポーツでは「ない」オートバイも必要ですから、2016年の販売台数トップ10(400cc以上)の中から国産のオートバイを抜き出したものも付け加えます。あと、もちろん機械学習と学習結果検証のコードが必要で、パーセプトロンの機械学習がどのように進むのかを可視化するコードもあったほうがよいでしょう。
というわけで、以下のコードを作ってきました。
import matplotlib.pyplot as plot # 動かす前に、pip install matplotlibしておいてください。
import matplotlib.animation as animation
from itertools import starmap
from random import shuffle
weights = (0.0, 1.0) # 重み
bias = -0.5 # バイアス
# パーセプトロン。
def perceptron(*xs):
# 入力と重みを掛け算して足しあわせてバイアスを足した結果が0以上なら1、そうでなければ0を返します。
return 1 if sum(x * weight for x, weight in zip(xs, weights)) + bias >= 0 else 0
# 機械学習用のデータ。
motorcycles = (
# ひとつ目の要素が1なら、スーパー・スポーツ。ふたつ目の要素は、排気量と価格のタプル。
(1, ( 999, 1501200)), # ホンダ CBR1000RR(ロスホワイト)
(1, ( 999, 1468800)), # ホンダ CBR1000RR
(1, ( 999, 1674000)), # ホンダ CBR1000RR<ABS>(ロスホワイト)
(1, ( 999, 2030400)), # ホンダ CBR1000RR SP
(1, ( 599, 1334880)), # ホンダ CBR600RR<ABS>(ロスホワイト)
(1, ( 599, 1162080)), # ホンダ CBR600RR(ロスホワイト)
(1, ( 599, 1302480)), # ホンダ CBR600RR<ABS>(グラファイトブラック)
(1, ( 599, 1129680)), # ホンダ CBR600RR(グラファイトブラック)
(1, ( 998, 2430000)), # ヤマハ YZF-R1(ライトれディッシュイエローソリット1)
(1, ( 998, 2376000)), # ヤマハ YZF-R1(ディープパープリッシュブルーメタリックC)
(1, ( 998, 3186000)), # ヤマハ YZF-R1M
(1, ( 998, 1782000)), # カワサキ Ninja ZX-10R
(1, ( 599, 924480)), # カワサキ Ninja ZX-6R
(1, ( 999, 1695600)), # スズキ GSX-R1000
(1, ( 999, 1760400)), # スズキ GSX-R1000 ABS
(1, ( 750, 1544400)), # スズキ GSX-R750
(1, ( 599, 1425600)), # スズキ GSX-R600
# ひとつ目の要素が0なら、スーパースポーツではない。
(0, ( 845, 1069200)), # ヤマハ MT-09 TRACER ABS
(0, ( 745, 743040)), # ホンダ NC750X
(0, ( 745, 793800)), # ホンダ NC750X<ABS>
(0, ( 745, 859680)), # ホンダ NC750X DCS<ABS>
(0, ( 745, 924480)), # ホンダ NC750X DCS<ABS> E Package
(0, ( 845, 1004400)), # ヤマハ MT-09 ABS
(0, ( 998, 1382400)), # ホンダ CRF1000L(ヴィクトリーレッド、パールグレアホワイト)
(0, ( 998, 1350000)), # ホンダ CRF1000L(キャンディープロミネンスレッド、デジタルシルバーメタリック)
(0, ( 998, 1490400)), # ホンダ CRF1000L DCS(ヴィクトリーレッド、パールグレアホワイト)
(0, ( 998, 1458000)), # ホンダ CRF1000L DCS(キャンディープロミネンスレッド、デジタルシルバーメタリック)
(0, ( 998, 1115640)), # スズキ GSX-S1000 ABS
(0, ( 998, 1166400)), # スズキ GSX-S1000F ABS
(0, ( 688, 760320)), # ヤマハ MT-07 ABS
(0, ( 688, 710640)), # ヤマハ MT-07
(0, ( 845, 1042200)), # ヤマハ XSR900
(0, (1164, 1172880))) # カワサキ ZRX1200 DAEG
# 機械学習します。
def train():
global weights, bias
# 排気量と価格を、0〜1の間の数値に正規化します。
min_xs = tuple(min(starmap(lambda _, xs: xs[i], motorcycles)) for i in range(2))
max_xs = tuple(max(starmap(lambda _, xs: xs[i], motorcycles)) for i in range(2))
normalized_motorcycles = [(label, tuple((xs[i] - min_xs[i]) / (max_xs[i] - min_xs[i]) for i in range(2))) for label, xs in motorcycles]
# 検証用データ(test_data)と学習用データ(train_data)に分けます。
shuffle(normalized_motorcycles)
test_data, train_data = normalized_motorcycles[:5], normalized_motorcycles[5:]
# 学習率(詳細は本文を参照してください)。
learning_rate = 0.01
# Matplotlibを使用して、可視化します。
# アニメーション用の変数。
figure = plot.figure()
images = []
# データを散布図として描画します。
plot.plot([xs[0] for label, xs in train_data if label == 0],
[xs[1] for label, xs in train_data if label == 0],
'bo',
marker='.')
plot.plot([xs[0] for label, xs in train_data if label == 1],
[xs[1] for label, xs in train_data if label == 1],
'ro',
marker='.')
plot.plot([xs[0] for label, xs in test_data if label == 0],
[xs[1] for label, xs in test_data if label == 0],
'bo',
marker='+')
plot.plot([xs[0] for label, xs in test_data if label == 1],
[xs[1] for label, xs in test_data if label == 1],
'ro',
marker='+')
# 学習。
for i in range(100):
images.append(plot.plot([-(weights[0] / weights[1]) * i - (bias / weights[1]) for i in range(2)], 'g'))
for label, xs in train_data:
result = perceptron(*xs)
if (result != label):
weights = tuple(weights[i] + learning_rate * (label - result) * xs[i] for i in range(len(weights)))
bias = bias + learning_rate * (label - result)
# 検証。
for label, xs in test_data:
print("{0}: {1}".format(label, perceptron(*xs)))
# 学習過程のアニメーションを表示します。
artist_animation = animation.ArtistAnimation(figure, images, interval=1, repeat_delay=1000)
# artist_animation.save('perceptron.gif', writer='imagemagick')
plot.show()
if __name__ == '__main__':
train()上のコードの中で機械学習部分のコードは、以下になります。
for label, xs in train_data:
result = perceptron(*xs)
# もし答えを間違えたら、重みとバイアスを調整します。
if (result != label):
weights = tuple(weights[i] + learning_rate * (label - result) * xs[i] for i in len(weights))
bias = bias + learning_rate * (label - result)重みの要素(weights[i])を、対応する入力の要素(xs[i])で増減(正解が1で答えが0なら1 - 0 = 1を掛けているので増、逆なら0 - 1 = -1を掛けるので減)させています。で、対応する入力の要素そのままだと重みやバイアスが大きく変わりすぎてしまいますので、学習率(learning_rate)に設定した適切な値(今回は0.01。適当に決めました)を掛け算しています。あと、データ数が少ないので、同じデータでの学習を100回繰り返しています。
こんな単純な処理ですけど、パーセプトロンはなんだかうまいこと機械学習してくれます。プログラムを実行すると、最終的に以下の検証コード
for label, xs in test_data:
# 「<正解データ>: <パーセプトロンの解答>」を表示します。
print("{0}: {1}".format(label, perceptron(*xs)))が実行され、コンソールに以下のような内容が表示されます。
0: 0
0: 0
1: 1
1: 1
0: 0
コロンの左が正解(スーパー・スポーツなら1、そうでなければ0)で、右がパーセプトロンが出した解答ですから、ほら、全問正解です(検証データの選択はランダムなので違う結果になることもありますけど、概ね正解できるはず)。
続いて、別ウィンドウが開いて以下の画像のようなアニメーションが表示されます。
X軸を排気量、Y軸を価格に(0〜1の数値になるように正規化しています)、スーパー・スポーツを赤、そうでない場合を青で描画しています。で、このウネウネと動いている緑の直線こそが、パーセプトロンが学習したスーパー・スポーツかそうでないかの境目なのです!
……えっと、「ちゃちい」とお感じになった皆様、皆様の感覚は正しいです。単純なパーセプトロンだと、データを直線で区切ることしかできないんですよ(重みや入力が3つの場合は、3次元空間上の2次元平面、4つの場合は4次元空間上の3次元の直線っぽい何かで二分します)。今回うまく学習できたのは、たまたまデータがパーセプトロンで判別できるものだったから。スーパー・スポーツのような高性能なオートバイは排気量あたりの価格が高い(あと、ボッタクリ価格の外車がデータに入っていない)から、直線で分別できただけなんですよ。
たとえば、体重と身長から長生きするかを判断する(身長が低い方が長生きする傾向にあるので直線で区切れそうに思えますけど、痩せすぎも太りすぎも長生きしそうにないので直線では区切れない)ような場合は、単純なパーセプトロンではダメ。複雑な判断が必要な多くの場合では、この単純なパーセプトロンは使用できないんですよ。
***身長と体重と長生きの図***
多層パーセプトロン(の理論)
だったら、パーセプトロンを組み合わせればよいのでは? 具体的には、以下の図のように積み重ねちゃえばよいのでは?
***パーセプトロンを積み重ねた図***
それはもちろんその通りなのですけど、ただ積み重ねるだけだと、単純なパーセプトロンの出力は0か1のどちらかであるという点が問題となります。重みやバイアスを調整しても出力が0のまま変わらなかったり、ほんの少し調整したら出力が0からいきなり1に変わったりするわけですから、この出力を受け取る次の層のパーセプトロンはとても混乱してしまい、学習どころではなくなってしまうでしょう。そもそも、パーセプトロンへの入力は実数なのですから、パーセプトロンからの出力が0または1だったら積み重ねようがありませんしね。
というわけで、パーセプトロンの出力を実数にしてみます。以下のような感じでしょうか?
def perceptron(*xs):
return sum(x * weight for x, weight in zip(xs, weights)) + bias残念! これだけではまだダメです。これだけだと、層を増やしても能力が上がらないらしいんですよ(私の数学力では、理由は全く分からなかったけど)。どこかに非線形な要素を追加しないとダメなんだってさ。
だから、「いきなり変わるのではなくてなだらかに変わる(そうじゃないと、学習できない)」かつ「非線形(そうじゃないと、複雑な分類ができない)」な、活性化関数と呼ばれる関数でくるんであげましょう。で、大昔からある活性化関数にシグモイド関数というのがあって、その出力は以下のような形をしています。
***シグモイド関数の図***
うん、なだらかかつ非線形ですな。ギャップがなくて直線じゃあないですもん。
あと、活性化関数にはもう一つ特徴が必要で、それは「微分できる」ことなんだそうです。重みやバイアスをどの程度変えたらどのように出力が変わるのかが微分で分かるみたいで、そうすれば積み重ねたニューロンを、前の層へ、出力から入力へと逆に調整していくことが可能になるんです。このような、微分をうまいこと使って誤差を逆向きに伝播させて学習させる方式を、逆誤差伝播学習法(バックプロパゲーション)と呼びます。
このような、出力を実数にして活性化関数を間に挟んで、逆誤差伝播学習法に類する方法で複数層のニューロンを学習させるという方式が、1960年代という大昔に発明されました。ただ、当時のコンピューターは貧弱すぎて、逆誤差伝播学習法はまともに動かなかったみたい。使えないものだからみんなすぐに忘れ、また誰かが再発明してまたみんなに忘れられ……という可哀想な運命を辿った挙句、1986年にラメルハートさんとその仲間たちが逆誤差伝播学習法と名づけたあたりで、コンピューターの性能が追いついてやっと定着したみたい。
ただ、先程紹介したシグモイド関数って、端の方にいくと変化が小さくなってしまうんですよ。変化が小さいということは微分した結果も小さくなるということで、伝播が少ないので学習が進みづらくなってしまいます。この問題を、勾配消失問題と呼びます。だから、今ではシグモイド関数はあまり使われていなくて、ReLUという以下の図のような特徴を持つ活性化関数が流行っています。
***ReLUの図***
この図は、先程のシグモイド関数の場合とは全然似ていませんけど、でも、これでも活性化関数として使えるんですよ。線はつながっているので、まぁなだらかと言えまるでしょう。0のところで曲がっていますから、線形ではありません。0のところは数学的には微分できませんけど、0か1のどちらかに決め打っちゃえば大丈夫(劣微分と言うそうです)。ほら、なんとかなりました!
実は、ReLUはとても優れた活性化関数です。勾配消失が発生しづらく、ニューロンを多段に重ね合わせることができます。単純なので、速く計算できます。あと、何といってもコードが簡単です。だから、活性化関数としてReLUを組み込んだパーセプトロンのコードなら、私ごときでもすぐに作成できます。
def perceptron(*xs):
return max(0, sum(x * weight for x, weight in zip(xs, weights)) + bias)……max(0, ...)を追加しただけです。max(0, ...)はとても簡単なコードですけど、これだけで先程のReLUの図になるでしょ?
ともあれ、これで逆誤差伝播学習法で学習させられる「多層パーセプトロン」が完成しました(対比のために、前節で述べたパーセプトロンは「単純パーセプトロン」と呼ばれます)。
実は、深層学習は、この多層パーセプロトンを逆誤差伝播学習法で学習させるところから始まっているんです(当時はReLUはなかったけど)。で、逆誤差伝播学習法が発表された1986年というのは、ファミリー・コンピューター用のドラゴンクエストIが発売された年なんです。2017年にPlay Station 4でファイナル・ファンタジーXVをやっている私達から見れば、旧世代のカンタンな技術なんですよ。
多層パーセプトロン(の実装)
でも、逆誤差伝播学習法で深層学習するには、微分をしなければなりません……。みなさんはどうか分かりませんけど、私は絶対に微分したくない。そもそも、私立文系の私は微分できないし! というわけで、ついにTensorFlowを使います。TensorFlowは自動微分の機能を持っているので、私のような数学が全くできない人間でも深層学習できるんですよ4。
MNIST
お題は、手書き文字認識としましょう。MNISTという、28×28ドットの手書き数字のデータ・セットを使用します。MNISTは、トレーニング用データを6万個、テスト用データを1万個もっています。
TensorFlowは、このMNISTをインターネットからダウンロードしてPythonで扱えるように変換してくれるクラスを含んでいますので、今回はそれを使って少しだけ楽をします。
import matplotlib.pyplot as plot # 動かす前に、pip install matplotlibしておいてください。
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
# MNISTデータ・セットを読み込みます。
train_data_set, validation_data_set, test_data_set = input_data.read_data_sets("MNIST_data/")
# データは、imagesとlabelsで取得できます。
print(len(train_data_set.images))
print(len(train_data_set.labels))
# next_batch()で、指定した数のデータを取り出せます。
images, labels = train_data_set.next_batch(5)
# 画像は、728個の実数のリスト。
print(images)
# ラベルは0から9の数値。
print(labels)
# 可視化してみます。
for image in images:
plot.imshow(np.reshape(image, (28, 28)))
plot.show()実際にデータを読み込む部分は、以下の1行だけです。簡単ですな。
train_data_set, validation_data_set, test_data_set = input_data.read_data_sets("MNIST_data/")read_data_sets()は、その内部で学習用と検証用、テスト用の3つのデータ・セットを生成します。MNISTそのものは学習用とテスト用の2つにしか分けていないのですけど、TensorFlowのMNIST関連ライブラリは、学習用データの一部を検証用データに分割してくれます(データを分割する理由は、あとで述べます)。
学習用と検証用、テスト用のデータ・セットは、imagesとlabelsという属性を持ちます。imagesは画像のリストで、画像は0.0〜1.0のデータを784個(28ドット×28ドット=784ドット)含むリスト形式になります。labelsは正解データのリストで、正解データは0〜9の数値です。
上のコードの最後の部分のようにMatplotlibとNumPyを使用して画像を可視化すると、以下のようになります。
これらの画像がどの数字なのかは、先に述べたlabelsを表示してみれば分かります。printしてみたら、7、3、4、6、1でした。
計算グラフ
先程実施した単純パーセプトロンの学習と同様で、学習というのは重みやバイアスといった変数の値を調整する作業になります5。で、変数を調整するための元ネタとして、単純パーセプトロンでは入力値を、今からやろうとしている逆誤差伝播学習法では微分を使うんでした。
この微分には、どのような計算がなされたのかという情報が必要なんだそうです。そういえば、はるか昔、私が高校時代にぶん投げた数学の教科書には「f(x,y)=小難しい式のとき、xで偏微分せよ」とか書いてあったような気がします。「どんな式かは分からないけど、フィーリングでいい感じで偏微分してちょうだいよ」というわけにはいかないみたい……。
だから、TensorFlowには計算式を定義する機能があります。計算式の定義は簡単で、TensorFlowが提供する演算子や関数と、変数作成機能を使って普通にプログラミングしていくだけ。具体的に、足し算を例に試してみましょう。
import tensorflow as tf
a = tf.Variable(1)
b = tf.Variable(2)
c = a + b
d = tf.Variable(3)
e = c + d
print(e)
print(e.op)tf.Variableは、TensorFlowで定義する計算式中で使用できる変数を表現するクラスです。TensorFlowの変数を使うと、+は通常のPythonの+ではなく、tf.add()に置き換えられます。で、変数cには、「計算結果」ではなくて「計算式」が代入されます。だから、print(c)しても3は表示されず、Tensor("add_12:0", shape=(), dtype=int32)のような変な文字列が表示されます。あと、print(c.op)してみると(opはOperationのop)、以下のような面白い内容が出力されます。
name: "add_12"
op: "Add"
input: "add_11"
input: "Variable_19/read"
attr {
key: "T"
value {
type: DT_INT32
}
}うん、なんとなく、どのような計算をするのかを表現していますな。面白いのは、一つ目のinputの値です。他の計算式(変数cが指し示す計算式)を指しています。計算式を、グラフ形式で表現しているわけ。このような計算を表現するグラフを、計算グラフと呼びます。
で、ここまでで定義した計算グラフを実行するには、TensorFlowのセッションが必要です。こんな感じ。
# セッションを作成します。
with tf.Session() as session:
# TensorFlowのVariableは、明示的に初期化しなければなりません。
session.run(tf.global_variables_initializer())
# 計算グラフを実行します。
result = session.run(e)
# 計算結果を表示します。
print(result)ここまでやって、やっと計算の実行結果である6が表示されます……。
2段階に分かれているので面倒に感じますけど、単純パーセプトロンのときみたいに学習のためのロジックを手書きするより遥かに簡単なので、我慢してください。そもそも、上の例のような順方向の計算だけなら、TensorFlowを使わずに普通に組めばよいわけですしね。
テンソル
ところで、先程計算グラフをprint()した時に表示された「Tensor」って何なのでしょうか? Wikipediaで調べてみると「線形的な量または線形的な幾何概念を一般化したもので、基底を選べば、多次元の配列として表現できるようなもの」と書いてあるけど、私にはなんだそれって感じ……。
まぁ、プログラミングのレベルでぶっちゃけていえば、テンソルってのは固定長の多次元の配列にすぎません。値そのものを表すスカラー(1や2.0)とかベクトル(平面座標[x, y]や立体座標[x, y, z])とか行列(3次元グラフィックスの座標変換で使うアレ)とかを統一的に扱う仕組みですな。スカラーはランク0、ベクトルはランク1、行列はランク2のテンソルと表現されます。カラー画像の場合だと、そのランクは3になります。X座標、Y座標、色(RGBA)と3つ指定してはじめて、その明るさが分かるわけですからね。普段プログラミングでやっている内容ですから、あまり身構えないでください。
で、なんでこのテンソルが深層学習で重要かというと、多層パーセプトロンをテンソルの演算として表現できるためです。単純パーセプトロンのとこでやったsum(x * weight for x, weight in zip(xs, weights)は、xsというテンソル(この場合はランク1なのでベクトル)とweightsというテンソル(同様にベクトル)に対するドット積(もしくは内積)と呼ばれる演算に相当します。だから、NumPyを使うなら上のコードはnp.dot(xs, weights)とかnp.inner(xs, weights)とかnp.matmul(xs, weights)とかのように、とても簡単に書けます。
パーセプトロンを複数個並べる場合だって、とても簡単です。たとえばパーセプトロンが2個なら、以下のような感じ。
import numpy as np
x = np.array([1, 2]) # 変数名がxsではなくてxなのは、テンソルはスカラーもベクトルも表現できちゃうので複数形の意味がなくなるため。
w = np.array([[3, 4], [5, 6]]) # 数学の本ではベクトルは太字にするみたいで、それを表現するために変数名を大文字にする数学プログラマーもいますけど、
b = np.array([7, 8]) # そこまで標準から外れるのは嫌なので、単数形でお茶を濁しました。
print(np.maximum(np.matmul(x, w) + b, 0)) # [20 24]が表示されます。wとbのランクを1つ上げて、最初の添字の要素数を2にするだけですな。私のようなビジネス・アプリケーションのプログラマーだとパーセプトロンのインスタンスを複数個作成してリストに格納してfor文等でそれぞれを呼び出すというやり方のほうが馴染み深いので変に感じますけど、こんなやり方もあるんですな。
ともあれ、このやり方なら、多層パーセプトロンの全てを計算式、つまりは計算グラフとして表現できることになります6。計算式が分かれば逆誤差伝播学習法が使えるわけで、これならもうすぐに深層学習できちゃう。
tf.contrib.layersとtf.nn、tf.train、tf
というわけで、機械学習可能な多層パーセプトロンをテンソルを使用する計算グラフとして定義……するのは、式がやたらと長くなるのでとても面倒くさいんです。だから、TensorFlowは深層学習の計算グラフを定義するための便利機能を提供しています。
まずは、tf.contrib.layers。tf.contrib.layersは、ニューラル・ネットワーク(多層パーセプトロンは、ニューラル・ネットワークの一つ)の層を生成する機能を提供します。たとえば、tf.contrib.layersの中のfully_connected()という関数は、多層パーセプトロンの層を一つ、まるっと作成してれます(どうしてこんな変な名前になっているかというと、後述する畳み込み層との対比で全結合層と呼ばれるようになったため……。深層学習とか数学は、歴史が長いので命名がバラバラで涙が出ちゃう)。linear()は活性化関数を含まないfully_connected()で、一番最後の層、出口で使います。
tf.nnは、ニューラル・ネットワーク特有の処理を提供します。たとえば、少し前で述べたReLUがrelu()関数として定義されています(tf.contrib.layers.fully_connected()等が勝手にReLUしてくれるので、直接使う場合は少ないですけど)。逆誤差伝播学習法での伝播していく大本の誤差はクロス・エントロピーと呼ばれる計算で出すとよいとされているのですど、そのクロス・エントロピーを計算してくれるsparse_softmax_cross_entropy_with_logits()なんてのもあります。あと、今回のように複数の選択肢の中のどれかを答えるような場合は、最後に「1」とか「2」と出力するのではなくて、「0かなーと思う数値は0.28」、「1だと思う数値は5.97」のように、10個の数値を出力する方式を採用します。そのような出力の場合に正解したかどうかを確認してくれるin_top_k()という関数もあります。
tf.trainという、訓練の際に役立つパッケージもあります。単純パーセプトロンのところで学習率について説明しましたけど、深層学習ではこの学習率の設定がやたらとシビアなんですよ。tf.trainの中には、この学習率をいい感じで自動設定(初期値を設定してくれるだけじゃなくて、適宜変更してくれる!)しながら訓練してくれるAdamOptimizerというクラスがあります。
で、これらのパッケージに含まれない基礎的な機能は、tfパッケージが提供します。placeholder()という、入力値や正解ラベルなどの、学習で調整されるわけでも定数でもない入れ物を作る関数とか、変数の初期化のためのglobal_variable_initializer()とか、平均を計算するreduce_mean()とかね。
TensorFlowを使用した、多層パーセプトロン
はい、ついに必要な知識が揃いました。TensorFlowで多層パーセプトロンを作成して、逆誤差伝播学習法で訓練してみましょう。
そのためのコードは、こんな感じ。
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# MNISTデータを取得します。
train_data_set, validation_data_set, test_data_set = input_data.read_data_sets("MNIST_data/")
# 画像と正解ラベル、トレーニング中かをを入れる変数を作成します。
images = tf.placeholder(tf.float32, (None, 784))
labels = tf.placeholder(tf.int32, (None,))
# ニューラル・ネットワークを定義します。TensorFlowでは、ニューラル・ネットワークの出力はlogitと呼びます。
logits = tf.contrib.layers.linear(tf.contrib.layers.fully_connected(images, 128), 10)
# logitとlabelsの誤差を計算します。TensorFlowでは、lossと呼びます。
loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=logits))
# どのように訓練するのかを定義します。lossが小さくなるように、学習率を自動で設定してくれるAdamOptimizerで訓練します。
train = tf.train.AdamOptimizer().minimize(loss)
# 正解率を計算します。
accuracy = tf.reduce_mean(tf.cast(tf.nn.in_top_k(logits, labels, 1), tf.float32))
# セッションを作成します。
with tf.Session() as session:
# tf.contrib.layersで変数を使用しているので、初期化します。
session.run(tf.global_variables_initializer())
# 3000回、訓練します。
for i in range(3000):
# 1回の訓練では、100個のデータを使用します。
images_value, labels_value = train_data_set.next_batch(100)
session.run(train, feed_dict={images: images_value, labels: labels_value})
# 精度の推移を知るために、100回に一回、訓練データと検証データでの精度を出力します。
if i % 100 == 0:
print("accuracy of train data: {0}".format(session.run(accuracy, feed_dict={images: images_value, labels: labels_value})))
print("accuracy of validation data: {0}".format(session.run(accuracy, feed_dict={images: validation_data_set.images, labels: validation_data_set.labels})))
# テスト・データでの精度を出力します。
print("accuracy of test data: {0}".format(session.run(accuracy, feed_dict={images: test_data_set.images, labels: test_data_set.labels})))MNISTのデータを取得する部分は、前に述べた通り。で、MNISTのデータをそのまま計算グラフに含めてしまうと毎回同じ計算をすることになってしまいますから、tf.placeholder()を経由させます。tf.placeholder()の第一引数は要素の型(MNISTの画像のデータは0.0〜1.0なのでtf.float32、正解ラベルは1とか2なのでtf.int32)、第二引数は要素の数です。画像は28x28=784個になるのですけど、その前にNoneと書かれています。これは、「バッチ学習」に対応するためです。
バッチ学習というのは、複数のデータを使用してバッチ的に学習する方式です。データ一つだけで訓練をすると、右往左往しちゃってなかなか学習が進まないんですよ。だから、複数のデータをまとめて計算して、その平均を誤差として扱うわけ。で、このバッチ学習のためにデータをまとめる数をあとから指定できるように、Noneを指定しています。
ニューラル・ネットワークの定義は、tf.contrib.layers.fully_connected()の結果をtf.contrib.layers.linear()しているだけ。計算式を知らなくても、実は余裕なんですな。で、TensorFlowではニューラル・ネットワークの計算グラフをlogitsという変数に格納する流儀になっている(理由は調べても分からなかった……)ので、それに従っています。
fully_connected()は前項で述べたように多層パーセプトロンの層の生成で、linear()は活性化関数を含まない多層パーセプトロンの層の生成です。引数は、入力となる計算グラフと生成したいパーセプトロンの数。ちなみに最後をlinear()にしているのは、どのような出力にするのかを後から選べるようにするためです。どの数字かを当てるだけならそのままでよいですし、1の可能性が83.4%、2の可能性が13.2%のように表示したいならtf.nn.softmax()してあげてください。今回のように逆誤差伝播学習法で学習したいなら、誤差計算のところのコードのようにtf.nn.sparse_softmax_cross_entropy_with_logits()してあげればよいでしょう。バッチ学習を採用していますから、上のコードでは各データの誤差を平均するためにtf.reduce_mean()しています。
次、訓練方法。学習率を考えるのは面倒なのでtf.train.AdamOptimizerを使って全て任せることにして、誤差を最小化するように訓練するのでminimize()を使用しています。あと、どの程度正解したかを確認するためのaccuracyという計算グラフも定義しておきます。tf.nn.in_top_k()は、logit[label]が、第三引数で指定した値の順位に収まっているかを調べます。logitsが[3, 2, 1]でlabelが2の場合、第三引数が1や2ならFalse、3ならTrueになるわけですね。で、バッチ学習なので結果はTrueやFalseのベクトルになって、PythonのTrueは1、Falseは0として扱えるので、tf.float32にキャストしてtf.reduce_mean()で平均を取れば正解率になります。
以上で計算グラフの定義は完了、あとは訓練するだけ。セッションを作成して、変数を初期化して、バッチ学習用のデータを取得して、訓練用の計算グラフであるtrainを実行します。trainが参照しているlossが参照しているlogitsの計算グラフにはimagesとlabelsが含まれていて、それらの値が確定しないと計算を開始できませんから、名前付き引数のfeed_dictでバッチ学習用のデータを渡しています。
で、これで必須の作業は終わりなのですけど、訓練の結果どの程度正解したかを知りたいですよね? だから、上のコードでは時々正解率を表示するようにしています。この正解率は、訓練データと検証データ、最後にはテスト・データの場合を表示しています。何度も似た情報を出力しているのは、訓練データに特化した学習(過学習といいます)をしてしまうのを避けるため。たとえばある問題集の解答を丸暗記すればその問題集には全問正解できるけれど、それでは他の問題集の問題は解けないので試験に落ちちゃって困っちゃう。訓練には使用していない検証データの正解率も調べておけば、訓練データでの正解率は向上しているけど検証データでの正解率は向上しないような場合に、過学習に陥ったと考えることができるわけ。検証データに加えてテスト・データでも正解率を出しているのは、ニューラル・ネットワークを調整(パーセプトロンの数を増やすとか、層を追加するとか)する際に、たまたま検証データに最適化した調整をしてしまうのを防ぐためです。検証データを使用して過学習を防いで、そのニューラル・ネットワークが本当に役に立つのかをテスト・データで検証していくわけですな。
はい、これで解説は終わり! プログラムを実行しましょう。少し時間がかかりますけど、最後に以下のような出力が出ます(重みやバイアスの初期値はランダムなので、結果は正確に同じにはなりません。でも、大体同じ値になるはず)。
accuracy of train data: 0.9900000095367432
accuracy of validation data: 0.9761999845504761
accuracy of test data: 0.9747999906539917テスト・データでの正解率が97.5%ってのは、なかなか良い結果だと思いませんか?
畳み込みニューラルネットワーク
でもね、もっと正解率を向上できるんです。そう、畳み込みニューラル・ネットワークならね。
畳み込み?
で、その畳み込みニューラル・ネットワークの「畳み込み」って、いったい何なのでしょうか?
畳み込みは、数学から考えずに、画像処理から考えると簡単に理解できます。画像処理といえばOpenCVというライブラリが思い浮かびますよね? この「OpenCV」に「畳み込み」を加えてGoogle検索すると、OpenCVの画像フィルタリングの機能がリスト・アップされます。このフィルタリングが、実は畳込みニューラル・ネットワークにおける「畳込み」なんですよ。
さっそく、フィルタリング(畳み込み)をやってみましょう。
import cv2 # 動かす前に、pip install opencv-pythonしておいてください。
import matplotlib.pyplot as plot # 動かす前に、pip install matplotlibしておいてください。
import numpy as np
# 画像を読み込んで、白黒に変換します。
image = cv2.cvtColor(cv2.imread('./images/lena.jpg'), cv2.COLOR_BGR2GRAY)
# とりあえず、何もしないで表示します。
plot.imshow(image)
plot.show()
# フィルタリング(畳み込み)します。
filtered_image = cv2.filter2D(image, -1, np.array([[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]]))
# フィルタリングした(畳み込んだ)結果を表示します。
plot.imshow(filtered_image)
plot.show()上のコードを実行すると、まずは読み込んだ以下の画像が表示されます。
この画像を閉じると、続いてフィルタリング(畳み込み)処理が実行され、たったこれだけのコードなのに、以下の輪郭が抽出された画像が表示されます。
なんかスゴイ! で、コードが短いのに輪郭抽出などという高度な処理ができるのは、OpenCVがcv2.filter2D()の中でとてもすごい処理をやっているから……ではありません。実は、cv2.filter2D()でやっているのは、第3引数のテンソルを掛け合わせて足し算しているだけなんですよ。
絵にするとこんな感じ。
***フィルタリングの絵***
コードにすると、こんな感じ。
import cv2 # 動かす前に、pip install opencv-pythonしておいてください。
import matplotlib.pyplot as plot # 動かす前に、pip install matplotlibしておいてください。
import numpy as np
image = cv2.cvtColor(cv2.imread('./images/lena.jpg'), cv2.COLOR_BGR2GRAY)
# 3x3でフィルタリング(畳み込み)しますので、-1と+1できるように、元画像を1ドットずつ大きくします。
scaled_image = cv2.copyMakeBorder(image, 1, 1, 1, 1, cv2.BORDER_CONSTANT, 0)
# フィルタリング(畳み込み)先の画像を用意します。
filtered_image = np.zeros(image.shape)
# フィルタリング(畳み込み)。
for i in range(filtered_image.shape[0]):
for j in range(filtered_image.shape[1]):
y = i + 1 # フィルタリング(畳み込み)元の位置は、1ドット右になります。
x = j + 1 # フィルタリング(畳み込み)元の位置は、1ドット下になります。
# フィルタリング(畳み込み)のドットを設定します。
filtered_image[i, j] = (scaled_image[y - 1, x - 1] * -1 +
scaled_image[y - 1, x ] * -1 +
scaled_image[y - 1, x + 1] * -1 +
scaled_image[y , x - 1] * -1 +
scaled_image[y , x ] * 8 +
scaled_image[y , x + 1] * -1 +
scaled_image[y + 1, x - 1] * -1 +
scaled_image[y + 1, x ] * -1 +
scaled_image[y + 1, x + 1] * -1)
# 画像なので、0〜255の間に限定します。
filtered_image[filtered_image < 0] = 0
filtered_image[filtered_image > 255] = 255
# 表示します。
plot.imshow(filtered_image)
plot.show()で、上のコードのドットを設定しているところの掛け算して足し合わせるという処理は、単純パーセプトロンのところでやったような気がしませんか? この手の処理は、たしかテンソルを使った簡潔な表現が可能でしたよね? 書き換えてみましょう。
w = [-1, -1, -1, -1, 8, -1, -1, -1, -1]
# フィルタリング(畳み込み)。
for i in range(filtered_image.shape[0]):
for j in range(filtered_image.shape[1]):
# フィルタリング対象のデータを抽出します。
x = np.reshape(scaled_image[i:i + 3, j:j + 3], (9,))
# 内積を求めて、フィルタリング(畳み込み)のドットを設定します。
filtered_image[i, j] = np.matmul(x, w)あらら。入力値の一部だけを使う点を除けば、これってパーセプトロンにそっくりじゃあないですか! で、パーセプトロンで重みを変更したら異なる場合の判断に使えたのと同様に、フィルタリング(畳み込み)も様々な目的に使えます。たとえば、w = [3, 0, -3, 3, 0, -3, 3, 0, -3]に変更してみると……
ほら、縦方向の線の抽出が可能になりました。もう少し工夫すれば、手書き文字認識に適したフィルタリング(畳み込み)ができるかもしれません。たとえば、角を強調するとか、交点を強調するとかね。
どのようなテンソルを与えれ手書き文字認識に適したフィルタリング(畳み込み)になるか分からない、そもそもフィルタリング(畳み込み)で何を強調すればよいのか分からない……という問題は、今までやってきたのと同様に、逆誤差伝播学習法による学習で解決しちゃえばよいというわけ。
プーリングとドロップアウト
あと、畳込みのあとに実施すると有効なテクニックとして、プーリングがあります。他に、いろいろなニューラル・ネットワークで有効なのだけどタイミングがなくて説明しそこねたテクニックとして、ドロップアウトがあります。畳込みニューラル・ネットのプログラミングをはじめる前に、この2つのテクニックを説明させてください。
プーリングとは、複数のデータをまとめる処理になります。たとえば、縦2ドット横2ドット、合計4ドット分の4つのデータを1つのデータに変換します。変換の方法として、昔は平均を取るのがよく使われていたみたいなのですけど、最近は、最大値を選択するマックス・プーリングが主流みたい。
このプーリングの利点としては、たとえば縦2ドット横2ドットでプーリングした場合に、入力画像における1ドットのズレを解消できることが挙げられます。座標(0, 0)が畳み込みで反応した場合も、座標(1, 1)が畳み込みで反応した場合も、プーリングすれば結果は同じになりますからね。たった1ドットと侮らないでください。畳み込みは繰り返し実施するとその効果が高まるらしいので、一般に何層か積み重ねます。畳み込み→縦横2ドットでのプーリングが2層なら元画像での4×4ドット、もう一層あれば8×8ドットの中に特徴的な箇所が含まれていれば大丈夫になるんですよ(まぁ、それでもたった8ドットなのですけど)。
プーリングには、データの質を保ったままで量を減らせるという効果もあります。畳込みで抽出したい特徴が10ある場合、結果は10枚の画像になるので、データ量が10倍になってしまうわけです。でも、その後に縦2ドット横2ドットでプーリングすれば、画像の大きさは縦も横も1/2なので1/4になり、元の2.5倍のデータ量におさまります。マックス・プーリングなら畳み込んだときに最も強く反応したデータが残りますから、プーリングでデータ量を減らしても特徴は保たれているでしょう。
次、ドロップアウト。これは、ニューラル・ネットワークの構成要素(ニューロン)を、一時的に一定割合で削除してしまうというテクニックです。
***ドロップアウトの絵***
ドロップ・アウトをやると、入力が欠けた状態でも正しく判断できるようにするために、データの細かな特徴ではなく、大きな特徴を学習するようになるらしいんですよ。テレビを見て、今受信しているのはどの放送局かを当てる人工知能で考えてみましょう。もしテレビの画面が正しく表示されているなら、右上に表示されている放送局のマークを見るだけで正解できます。だから、画面の他の部分は見ずに、右上だけを凝視して回答するようになってしまうでしょう。でも、もしテレビ画面の半分ぐらいが映らないなら? その映らない範囲が、ランダムに変わるとしたら? これなら、画面全体をまんべんなく見て、大きなお友達向けのアニメが流れているからTokyo MXかなぁと答えられる、より汎用的な人工知能に育ってくれるでしょう。
tf.contrib.layers再び
さて、前節で多層パーセプトロンを作成したときは、tf.contrib.layers.fully_connected()を使用しました。畳み込みニューラル・ネットワークの作成では、同様にtf.contrib.layers.conv2d()を使用します。マックス・プーリングはtf.contrib,layers.max_pool2d()。ドロップアウトも、tf.contrib.layers.dropout()を呼び出すだけで実現できます。モーレツに楽ちんですな。
もう少し。MNISTのテンソルは1次元なのですけど、畳み込みニューラル・ネットワークの処理対象のテンソルは2次元です。この変換はtf.reshape()を使えば実現できます。逆向きの変換は、tf.flatten()です。
TensorFlowを使用した、畳み込みニューラル・ネットワーク
では、待ちに待った畳込みニューラル・ネットワークをしてみましょう。コードは、こんな感じ。
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
# MNISTデータを取得します。
train_data_set, validation_data_set, test_data_set = input_data.read_data_sets("MNIST_data/")
# 画像と正解ラベル、トレーニング中かをを入れる変数を作成します。
images = tf.placeholder(tf.float32, (None, 784))
labels = tf.placeholder(tf.int32, (None,))
is_training = tf.placeholder_with_default(False, ())
# ニューラル・ネットワークを定義します。TensorFlowでは、ニューラル・ネットワークの出力はlogitと呼びます。
logits = tf.reshape(images, (-1, 28, 28, 1))
logits = tf.contrib.layers.conv2d(logits, 32, 5)
logits = tf.contrib.layers.max_pool2d(logits, 2)
logits = tf.contrib.layers.conv2d(logits, 64, 5)
logits = tf.contrib.layers.max_pool2d(logits, 2)
logits = tf.contrib.layers.flatten(logits)
logits = tf.contrib.layers.fully_connected(logits, 128)
logits = tf.contrib.layers.dropout(logits, is_training=is_training)
logits = tf.contrib.layers.linear(logits, 10)
# logitとlabelsの誤差を計算します。TensorFlowでは、lossと呼びます。
loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=logits))
# どのように訓練するのかを定義します。lossが小さくなるように、学習率を自動で設定してくれるAdamOptimizerで訓練します。
train = tf.train.AdamOptimizer().minimize(loss)
# 正解率を計算します。
accuracy = tf.reduce_mean(tf.cast(tf.nn.in_top_k(logits, labels, 1), tf.float32))
# セッションを作成します。
with tf.Session() as session:
# tf.contrib.layersで変数を使用しているので、初期化します。
session.run(tf.global_variables_initializer())
# 3000回、訓練します。
for i in range(3000):
# 1回の訓練では、100個のデータを使用します。
images_value, labels_value = train_data_set.next_batch(100)
session.run(train, feed_dict={images: images_value, labels: labels_value, is_training: True})
# 精度の推移を知るために、100回に一回、訓練データと検証データでの精度を出力します。
if i % 100 == 0:
print("accuracy of train data: {0}".format(session.run(accuracy, feed_dict={images: images_value, labels: labels_value})))
print("accuracy of validation data: {0}".format(session.run(accuracy, feed_dict={images: validation_data_set.images, labels: validation_data_set.labels})))
# テスト・データでの精度を出力します。
print("accuracy of test data: {0}".format(session.run(accuracy, feed_dict={images: test_data_set.images, labels: test_data_set.labels})))多層パーセプトロンのコードとの違いは、is_trainingというドロップアウト制御用の変数が追加されたことと、ニューラル・ネットワークの定義部分が異なっているところだけです。
tf.contrib.layers.conv2d()に渡すテンソルは、[バッチ数, 高さ, 幅, チャンネル数]という形です。で、バッチ数は実行時に指定したいから-1にします(placeholder()のときはNoneだったのに、reshape()では-1なんですよ。このような不整合は勘弁して欲しい……)。MNISTのデータは高さも幅も28。白黒なので、チャンネル数は1です(カラーなら赤と緑と青で3チャンネル。透明度が加わったRGBAなら4チャンネル)。というわけで、tf.reshape()の第二引数は(-1, 28, 28, 1)となります。あと、reshape()の結果をconv2d()の引数にして……と書くと横方向に際限なく長くなってしまうので、上のコードでは一時変数を使用して縦に並べました(F#の|>やClojureの->みたいな、左から右にコードを書く仕組みが欲しい……。まぁ、どうしようもないけど)。
tf.contrib.layers.conv2d()の引数は、第二引数が出力するチャンネルの数で、第三引数がフィルタリング(畳み込み)するフィルターの大きさです。チャンネルの数には、画像の場合のような制約(画像は白黒かカラーか透明度付きカラーしかないので、チャンネル数は1か3か4のどれかになる)はありません。抽出したい特徴の数をご指定ください(現場では、とりあえず適当な数を設定して、適当に増減させて調節することになります)。フィルターのサイズは、抽出したい特徴の大きさになります(こちらも、現場では当てずっぽうで設定してあとで調整します)。
tf.contrib.layers.max_pool2d()の引数は、データをまとめる大きさです。2を指定すれば、縦横2ドットのデータを一つにまとめます。conv2d()とmax_pool2d()は、複数回繰り返すと大きく性能が上がるみたいです。なので、上のコードでは2回繰り返してみました。
で、畳み込みやプーリングの結果は、やっぱり画像になります。これをそのまま出力したのでは手書き文字認識にはなりませんから、畳込みの結果を入力にして、多層パーセプトロンをつなげます。多層パーセプトロンの入力は1次元のデータなので、tf.contrib.layers.flatten()した後にtf.contrib.layers.fully_connected()しています。
あとは、精度が高くなるという噂を信じてtf.contrib.layers.dropout()を入れて、tf.contrib.layers.linear()で終わりです。そうそう、dropout()は、名前付き引数のis_trainingがTrueならばニューラル・ネットワークの一部を削除し、Falseならば削除しません。訓練のときはis_training=True、正解率を確認するときはis_training=Falseにしたいので、is_trainningはtf.placeholder_with_defrault()でデフォルト値をFalseにして作成し、訓練のときだけfeed_dictを通じて値を設定するようにしています。
以上、これでコードの解説終わり! プログラムを実行しましょう。かなり長い時間がかかりますから、トイレに行ったりコーヒー飲んだりしてみてください。で、その結果はどうかというと……
accuracy of train data: 1.0
accuracy of validation data: 0.9911999702453613
accuracy of test data: 0.9919999837875366テスト・データでの正解率が99%を超えて、99.2%になりました! 深層学習って、スゴイですな。
プログラムの作成
このようにスゴイ深層学習を、実際のプログラムで試してみたくなりませんか? 私は試してみたくなっちゃったので、今回は、ロボットに一般物体認識させるプログラムを作ってきました。
で、その作ってきたソース・コードは、皆様が深層学習するときのテンプレートとして使用できるんじゃないかなーと考えてています。本章で紹介するソース・コードを少し修正するだけで、皆様が深層学習でやりたいことを実現できるんじゃないかなーと。
ソース・コードの取得方法
そんなことを言われても、現物を見なければ信用できない? その通りですね。ソース・コードはGitHubで管理していますので、Git for Windowsをセットアップして、以下のコマンドでダウンロードしてみてください。
> cd TensorFlow # 「2. インストール」で作成した、TensorFlow用の仮想環境
> git clone https://github.com/tail-island/jellyfish-eye.gitなお、プロジェクト名のjellyfish-eyeは、一部のクラゲは高機能な目を持っていて、脳がないにもかかわらず目で見た情報を処理して活用しているらしいことに由来しています。脳を持たないロボットがセンサーの情報にもとづいて自律行動しちゃうぜって感じですね。
Pythonのパッケージ
Pythonでは、ファイルがパッケージの単位になります。./aaa.pyの中から./bbb.pyに含まれる関数を使う際には、import bbbしてあげなければならないわけですね。
今回は、あとでコードを活用できるように、複数のファイルに分割しました。その際に、管理が楽になるように専用のパッケージを作成しています。./xxx/ccc.pyのようにプログラム・ファイルをディレクトリの下に配置して、import xxx.cccにしたわけですな。
このパッケージ名(ディレクトリ名)は、Pythonの文法に合うようにプロジェクト名の-を_に置換して、jellyfish_eyeとしました。
> mkdir jellyfish_eyeデータ
私の手持ちのロボットはTurtleBotというロボットで、これには深度センサー(MicrosoftのKinectのような、ドット単位で、センサーから物体までの距離を計測できるもの)が付いています。
***絵***
この深度センサーのデータを深層学習で処理して、ロボットの前に置かれたものが何かを回答することにしましょう(本稿では紹介しませんけど、実は、指定した物体の前まで移動するロボット用のプログラムも書いています。ロボットがモノを見分けるように見えて少し不気味なデモをご覧になりたい場合は、声をかけてください)。
ロボットの回答のレベルについても、考えなければなりません。というのも、「ギター」と回答するのか「平沢唯のギー太」と回答するのかで、難しさが変わってくるんですよ。「平沢唯のギー太」で学習して「平沢唯のギー太」を見つけるのは特定物体認識と呼ばれる技術で、実はけっく簡単で面白くない。だから、「平沢唯のギー太」で学習して、学習データには含まれていなかった「中野梓のフェンダーJAPANのムスタング」でも「ギター」と答えてくれる、一般物体認識をやることにしましょう。
以上で方針が決まりましたので、学習用のデータを作成しました。ロボットの上でプログラムを動かして、縦100ドット×横100ドット×(3原色+距離)のデータを、「カレンダー」と「飲み物」、「ティッシュ・ペーパー」の3種類分、集めてきました(会社の机の周りにあった、適当なモノを3種類です)。
データを格納しているディレクトリーの構造は、以下の通り。
data
├── calendar
│ ├── DISNEY_カレンダー
│ ├── UNITED_BEES_カレンダー
│ └── software_AG_カレンダー
├── drink
│ ├── GEORGIA_EUROPIAN_香るブラック_400ml
│ ├── KIRIN_真っ赤なベリーのビタミーナ
│ ├── NATIONAL_VENDING_ホット
│ ├── POKKA_SAPPORO_冴えるBLACK_275g
│ ├── POKKA_SAPPORO_冴えるBLACK_400g
│ ├── POKKA_SAPPORO_富士山麓の天然水
│ └── SUNTORY_伊右衛門_1l
└── tissue-paper
├── FamilyMart_フェイシャルティシュー
├── Kleenex_HIGH_QUALITY_FACIAL_TISSUES
├── Kleenex_ハイクオリティ_フェイシャルティシュー
├── Kleenex_ローションティシュー_エックス
└── エリエール_贅沢保湿
三階層目のディレクトリーの中に、それぞれの物体のデータが入ります。様々な角度から、1物体につき複数のデータを採取しています。
データの単位で、ファイルは分割されています(ファイルの拡張子は.txt)。そのファイルの1行は1ドットを表していて「赤<空白>青<空白>緑<空白>距離」となっています(それぞれのデータの範囲は、0.0〜1.0)。最初の行は左上のドット、次の行はその右のドットと続き、最も右下のドットまで、10000行続いています。
jellyfish_eye/data_sets.py
今回は独自のデータを使用していますから、前章のMNISTの場合のような便利データ管理クラスはありません。ですから、データ管理のコードを作成しました。
import numpy as np
import os
from functools import partial
from itertools import chain, islice, repeat, starmap, tee
# データ管理用のクラスです。
class DataSet:
# データをシャッフルします。
def _shuffle(self):
indice = np.arange(len(self.inputs))
np.random.shuffle(indice)
self.inputs = self.inputs[indice]
self.labels = self.labels[indice]
# コンストラクタ。inputs_collectionは、[[クラス0のデータ, クラス0のデータ...], [クラス1のデータ, クラス1のデータ], ...]としてください。
def __init__(self, inputs_collection):
self.inputs = np.array(tuple(chain.from_iterable(inputs_collection)))
self.labels = np.array(tuple(chain.from_iterable(starmap(lambda i, inputs: repeat(i, len(inputs)), enumerate(inputs_collection)))))
self._shuffle()
self._batch_index = 0
# バッチ学習用のデータを取得します。
def next_batch(self, batch_size):
if self._batch_index + batch_size > len(self.inputs):
self._shuffle()
self._batch_index = 0
start = self._batch_index
end = self._batch_index = self._batch_index + batch_size
return self.inputs[start:end], self.labels[start:end]
# データを読み込みます。
def load(data_path='./data'):
# データを1行分、読み込みます。
def rgbz(line_string):
# 赤と緑と青と距離に分割し、数値化します。
return map(float, line_string.split())
# データを1つ、読み込みます。
def load_image(path):
# 一行ずつ読み込んで、chain.from_iterableでフラットにします。
with open(path) as file:
return tuple(chain.from_iterable(map(rgbz, file)))
# 物体のデータを読み込みます。
def load_object(object_path):
# 様々な角度からのデータを読み込みます。
return map(load_image, filter(lambda path: '.txt' in path, map(partial(os.path.join, object_path), sorted(os.listdir(object_path)))))
# クラス(カレンダーや飲み物)を読み込みます。
def load_class(class_path):
# 物体単位でデータを読み込んで、chain.from_iterableでフラットにします。
return tuple(chain.from_iterable(map(load_object, map(partial(os.path.join, class_path), sorted(os.listdir(class_path))))))
# 訓練データとテスト・データに分割します。
def train_and_test(images):
# 20番目以降を訓練データ、それより前をテスト・データとします。
return images[20:], images[:20]
# クラス(カレンダーや飲み物)単位でデータを読み込んで、訓練データとテスト・データに分割し、データ・セットのクラスを生成します。
return map(DataSet, zip(*map(train_and_test, map(load_class, map(partial(os.path.join, data_path), sorted(os.listdir(data_path)))))))上半分のclass DataSetは、深層学習を使う目的がクラス分類ならば、どのようなデータでも使えると思います。お好きにコピー&ペーストしてみてください。_shuffle()等でNumPyの面白機能を使っていて、NumPyの奥深さを味わえますな。
下半分のload()は、対象データが異なれば、書き直しになるでしょう。その書き直しの負荷を減らすにはソース・コードの量を減らすのが有効で、そのためには関数型プログラミングが効く。だから、Pythonの公式文書の関数型プログラミング HOWTOを読んで、関数型で書いてみました。関数型プログラミング HOWTOは非常にわかりやすいので、一読するだけで、上のコードのような感じに関数型プログラミングでコード量を削減できますよ。あと、今回は採取できたデータ量が少なかったので、訓練用とテスト用の2つのデータ・セットしか使用しないことにしました。
jellyfish_eye/model.py
次は、ニューラル・ネットワークを定義することにしましょう。以下がそのコード。
import tensorflow as tf
from jellyfish_eye.utilities import summary_image, summary_image_collection, summary_scalar
# 必要なplaceholderを定義します。
inputs = tf.placeholder(tf.float32, (None, 100 * 100 * 4))
labels = tf.placeholder(tf.int32, (None,))
is_training = tf.placeholder_with_default(False, ())
# TensorFlowのドキュメントに、ニューラル・ネットワークを作成する関数名はinference(推論)にしろと書いてありました。なのでinference()。
def inference():
# summary_image()とsummary_image_collection()は、次の項で説明します。ログを取ると思ってください。
# 畳み込みできるように、データの形を変更します。
outputs = summary_image('input', tf.reshape(inputs, (-1, 100, 100, 4)))
# 畳み込みとプーリング(1層目)
outputs = summary_image_collection('convolution-1', tf.contrib.layers.max_pool2d(tf.contrib.layers.conv2d(outputs, 32, 10), 2))
# 畳み込みとプーリング(2層目)
outputs = summary_image_collection('convolution-2', tf.contrib.layers.max_pool2d(tf.contrib.layers.conv2d(outputs, 64, 10), 2))
# 全結合層で処理できるように、データの形を変更します。
outputs = tf.contrib.layers.flatten(outputs)
# 全結合層。1024ニューロンと512ニューロンの2層。
outputs = tf.contrib.layers.stack(outputs, tf.contrib.layers.fully_connected, (1024, 512))
# ドロップアウト。
outputs = tf.contrib.layers.dropout(outputs, is_training=is_training)
# 3クラスに分類します。
return tf.contrib.layers.linear(outputs, 3)
# 損失関数。
def loss(logits):
# summary_scalarは、次の項で説明します。ログを取るんだと思ってください。
# クロス・エントロピーで、損失を計算します。
return summary_scalar('loss', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=labels)))
# 訓練。
def train(loss):
# 学習率の設定が不要なAdamOptimizerを使用して訓練します。
return tf.train.AdamOptimizer().minimize(loss)
# 精度。
def accuracy(logits):
return tf.reduce_mean(tf.cast(tf.nn.in_top_k(logits, labels, 1), tf.float32))前章でやった畳み込みニューラル・ネットワークのコードから、ニューラル・ネットに関係する部分を抜き出して、関数化しただけです。
で、これらの処理をわざわざ別のパッケージに抜き出した理由は、変更が多い部分を局所化するためです。深層学習してみたのだけど精度が低いという場合は、ニューラル・ネットワークをチューニングしなければなりません。で、チューニングでは何をするかというと、max_pool2d()やconv2d()、fully_connected()の引数を変更することになります。畳み込みで出力されるチャンネル数を増やしたりとかね。あとは、畳込み層を増やすなんてのも、よくやります。これらのチューニング作業が、このmodel.pyの修正だけで済むというわけ。
jellyfish_eye/utilities.py
さて、TensorFlowはとても良くできているのですけど、でも、痒いところ全てに手が届くような環境ではありません。あと、Python流のやり方の中には、関数型プログラミング HOWTOを熟読しちゃって関数型になった後だと気持ち悪く感じるものもあります。
というわけで、ユーティリティーをいくつか作ってきました。
import tensorflow as tf
# 画像をTensorBoardで確認できるようにするために、サマリーに追加します。
def summary_image(name, variable):
# 一つ目の画像だけを抜き出して、サマリーに追加します。
tf.summary.image(name, tf.slice(variable, (0, 0, 0, 0), (1, -1, -1, -1)))
# 呼び出し時に一時変数を作らなくて済むようにするために、variableをリターンしておきます。関数型だと、一時変数は気持ち悪い。
return variable
# 畳み込み後のような複数のチャンネルに別れた画像を、タイル状に並べてサマリーに追加します。
def summary_image_collection(name, variable):
# 画像の高さ、幅、タイル状に並べる際の行と幅を取得します。
h = tf.shape(variable)[1]
w = tf.shape(variable)[2]
r = tf.shape(variable)[3] // 8 # 畳込みの出力チャンネル数は、8の倍数にしてください……。
c = 8
# 一つ目の画像を、チャンネル単位にタイル状に並べます。
image = tf.slice(variable, (0, 0, 0, 0), (1, -1, -1, -1))
image = tf.reshape(image, (h, w, r, c))
image = tf.transpose(image, (2, 0, 3, 1))
image = tf.image.adjust_contrast(image, 1)
image = tf.reshape(image, (1, r * h, c * w, 1))
# サマリーに追加します。
tf.summary.image(name, image)
# 呼び出し時に一時変数を作らなくて済むようにするために、variableをリターンしておきます。
return variable
# スカラー(1.0や'abc'などの、単一の値)を、サマリーに追加します。
def summary_scalar(name, variable):
# サマリーに追加します。
tf.summary.scalar(name, variable)
# 呼び出し時に一時変数を作らなくて済むようにするために、variableをリターンしておきます。
return variable……えっと、サマリー関連だけですな。ユーティリティーが少ないのは、私が手を抜いたからではなく、TensorFlowの完成度が高いためです。
さて、summary_image()やsummary_image_collection()するとTensorFlowの可視化ツールであるTensorBoardで、深層学習がどのように働くのかを以下のような形で見る事ができます。
前項のように損失関数をsummary_scalar()しておけば、値がどのように変化したのかが、以下のようにグラフで見ることができます。
jellyfish_eye/train.py
以上で、道具が揃いましたから、深層学習させるコードを書きましょう。
import jellyfish_eye.data_sets as data_sets
import jellyfish_eye.model as model
import tensorflow as tf
# 学習用データを取得します。
train_data_set, test_data_set = data_sets.load()
# モデルで定義したplaceholderを取得します。
inputs = model.inputs
labels = model.labels
is_training = model.is_training
# ニューラル・ネットワークを定義します。
logits = model.inference()
train = model.train(model.loss(logits))
accuracy = model.accuracy(logits)
# 学習に必要な環境を整えます。
global_step = tf.contrib.framework.get_or_create_global_step() # 何ステップ目の学習なのかを表現します。
inc_global_step = tf.assign(global_step, tf.add(global_step, 1)) # global_stepに1を足してインクリメントします。
summary = tf.summary.merge_all() # TensorBoardへの出力をまとめます。
supervisor = tf.train.Supervisor(logdir='logs', save_model_secs=60, save_summaries_secs=60, summary_op=None) # 学習全体を管理します。
# Supervisor経由で、セッションを取得します。
with supervisor.managed_session() as session:
# Supervisorが問題を検出しなければ、学習を繰り返します。
while not supervisor.should_stop():
# グローバル・ステップの値を取得します。
global_step_value = session.run(global_step)
# バッチ学習で、ニューラル・ネットワークを訓練します。
train_inputs, train_labels = train_data_set.next_batch(20)
session.run(train, feed_dict={inputs: train_inputs, labels: train_labels, is_training: True})
# 一定のタイミングで、サマリーを取得したり、精度を出力したりします。
if global_step_value % 10 == 0:
# サマリーを取得します。
supervisor.summary_computed(session, session.run(summary, feed_dict={inputs: train_inputs, labels: train_labels, is_training: True}))
# 精度を出力します。
print('global step {0:>4d}: train accuracy = {1:.4f}, test accuracy = {2:.4f}.'.format(
global_step_value,
session.run(accuracy, feed_dict={inputs: train_inputs, labels: train_labels}),
session.run(accuracy, feed_dict={inputs: test_data_set.inputs, labels: test_data_set.labels})))
# グローバル・ステップをインクリメントします。
session.run(inc_global_step)学習する「だけ」なら、前章のコードのような簡単なものでも良いのですけど、学習して精度を表示して終わりで後には何も残らないんじゃ意味がないですよね? そう、学習した結果を活用する、サービスまで作らないとね。あと、学習の途中でコンピューターが落ちたら最初からやり直しではキツイので、途中から学習を再開できる仕組みも必要。学習が良い感じに進んでいるかどうか、目で見て確認もしたいですよね。
というわけで、tf.train.Supervisorを使いましょう。Supervisorを使うと、学習したモデルをsave_model_secsで指定したタイミングで保存してくれます。そして、Supervisor経由でセッションを取得すれば、最新の学習結果(logs/checkpointというファイルの中で指定すれば、最新でなくても可)で初期化されたモデルが手に入ります。これなら、保存された学習結果を活用するサービスを作れますし、学習の途中で中断して、その後に再開することもできます。サマリーもsave_summaries_secsで指定した間隔で保存してくれますから、前述したモデルの中でサマリーに追加した情報を見ることもできます。
たしかに、Supervisorを使うにはglobal_stepの作成が必要だったり、global_stepをインクリメントする処理が必要だったり、summary_op=Noneしておかないとfeed_dictが必要なサマリーは使えなかったりとか、少し面倒なところはあります。でも、その面倒は、上のコードをコピー&ペーストすればゼロになります。対象データやニューラル・ネットワークの内容が異なっても訓練のやり方は変わりませんから、上のコードのimportする名前空間を変更すれば、皆様のプロジェクトでも使えると思いますよ。
jellyfish_eye/serve.py
ついでですから、サービス化の前準備もしてしまいましょう。深層学習した結果を活用して、物体が何なのかを判断する関数を作成します。
import jellyfish_eye.model as model
import numpy as np
import tensorflow as tf
# モデルで定義したplaceholderを取得します。
inputs = model.inputs
# ニューラル・ネットワークを定義します。確率で表現したいので、tf.nn.softmaxをtf.nn.top_kとinferenceの間に入れます。
top_k = tf.nn.top_k(tf.nn.softmax(model.inference()), k=3)
# サービスに必要な環境を整えます。
supervisor = tf.train.Supervisor(logdir='logs', save_model_secs=0, save_summaries_secs=0)
session = supervisor.PrepareSession() # セッションを使いまわしたいので、managed_sessionではなくてPrepareSession()。なんで大文字なんだろ?
# クラスを判断します。
def classify(input):
return session.run(top_k, feed_dict={inputs: np.array((input,))})
# ダミーの実行モジュール。削除しても構いません。
if __name__ == '__main__':
import jellyfish_eye.data_sets as data_sets
import matplotlib.pyplot as plot
import time
_, test_data_set = data_sets.load()
classes = []
starting_time = time.time()
for input in test_data_set.inputs:
classes.append(classify(input))
finishing_time = time.time()
print('Elapsed time: {0:.4f} sec'.format((finishing_time - starting_time) / len(test_data_set.inputs)))
for input, label, class_ in zip(test_data_set.inputs, test_data_set.labels, classes):
print('{0} : {1}, {2}'.format(label, class_.indices[0][0], tuple(zip(class_.indices[0], class_.values[0]))))
plot.imshow(np.reshape(input, (100, 100, 4)))
plot.show()前項で紹介したSupervisorを使えば、サービスの提供も簡単に実現できます。セッション取得のための時間を節約するために、managed_session()ではなくPrepareSession()するところに注意するだけ。あとは、バッチ学習じゃないので、inputからinputsに変換するために形を変換する程度。上のコードの上半分をコピー&ペーストすれば、ほとんどのプロジェクトで使えると思いますよ。
なお、このコードは、以下のようにして他のパッケージから深層学習の結果を利用することを想定しています。
import jellyfish_eye.serve
...
image = ... # 深度センサーからデータを取得。
if jellyfish_eye.serve(image).indices[0][0] == 0: # ディレクトリの名前順でクラスを決めたので、0がcalendarで1がdrink、2がtissue-paper。
# カレンダーが前に置かれたので、予定をチェックする等の処理を実行する。この利用する側のコードは、頑張って作ってくださいね。
実行
以上で、全てのコードが完成です。さっそく実行してみましょう。Pythonでパッケージ名を指定して実行するときはpython -m <パッケージ名>とします。今回のコードの場合は、訓練したいならpython -m jellyfish_eye.train、サービスを提供するならpython -m jellyfish_eye.serveとなります。
さて、注目の深層学習の結果は……
うん、精度が80%を超えました! かなり良いんじゃないでしょうか?
なお、GPUを積んでいない私の4年前のおんぼろラップトップPCだとここまで訓練するのに1時間くらいかかりましたが、NvidiaのGeForce GTX 980 Tiという1年前に10万円で買ったGPUを積んだPCで実験してみたら、たった1分で終わっちゃいました……。2017年3月現在なら980より圧倒的に速いというGeForce GTX 1080が8万円くらいで買えますから、もし深層学習に興味を持ったならGPUの購入をおすすめします。
あと、学習している間が暇だなーと感じるようでしたら、TensorFlowのツールであるTensorBoardで学習内容を可視化してみてください。TensorBoardの実行は、tesnsorboard --logdir=logsです。TensorBoardが起動したら、Webブラウザを開いてURLにlocalhost:6006と入れればオッケー。こうすると、前の項で見ていただいたような畳込み結果や入力画像を見られたり、損失関数がどんどん減っている(学習がどんどん進んでいる)ことが分かるので、あまり退屈しないで済みます。
さて、訓練が終わったので、その結果をサービス化してみると……
うん、正しく判別できていますね(左半分のガタガタの絵は、距離を透明度に変換して画像化した入力データです)。深層学習の「学習」には時間がかかりますけど、深層学習した結果を実行するだけなら、GPUなしでもかなり速いです。一回の判断に0.067秒しかかかっていないんですからね。これなら、私のロボットに積んでいるRaspberry Piでも、なんとか実行できそうですな。
おわりに
ほら、深層学習って、簡単でしょ? 本稿のコードをコピー&ペーストして、データを取得する部分を書き直して、必要に応じてニューラル・ネットワークをチューニングするだけでよいんですから。
多層パーセプトロンや畳み込み、プーリング、ドロップアウトという深層学習で使われる道具についても、数学なしでその概念を理解できました7。概念が分かっているのですから、ニューラル・ネットワークのチューニングは可能なはず8。逆誤差伝播学習法の詳細になると数学が必要なのですけど、TensorFlowが全自動でやってくれるから詳細を理解していなくても問題ないしね9。ほらやっぱり、深層学習って簡単でしょ?
でも所詮、深層学習って画像が何なのかを判断できるるだけなのでは? データが画像以外だったらどうするの……という反論が聞こえてきそうですけど、深層学習は画像以外でも使えるのでご安心ください。私は先日、ひょんなことから「超音波センサー10個の距離情報から、トイレの個室でのスマートフォン使用を検知する」システムのプロトタイプを作ったのですけど、適当に取った数人×数分のデータを以下のニューラル・ネットワークに食わせて学習してみたら、70%を超える精度が出ちゃいました。遊びでやったので作業はそこで止まってますけど、データを増やせば、実用になるんじゃないかな。
def inference():
outputs = tf.reshape(inputs, (-1, data_sets.history_size, 1, data_sets.channel_size)) # 幅を1にして、1次元データをconvolution2dできるようにします。
outputs_list = [tf.contrib.layers.max_pool2d(tf.contrib.layers.convolution2d(outputs, 128, (kernel_size, 1), padding='VALID'), (data_sets.history_size - kernel_size + 1, 1)) for kernel_size in (5, 4, 3, 2)]
outputs = tf.concat(1, [tf.contrib.layers.flatten(outputs) for outputs in outputs_list]) # 次元0はバッチなので、次元1でconcatします。
outputs = tf.contrib.layers.stack(outputs, tf.contrib.layers.fully_connected, (1024, 512))
outputs = tf.contrib.layers.dropout(outputs, is_training=is_training)
return tf.contrib.layers.linear(outputs, 2)ちなみに、このニューラル・ネットワークは、「自然言語処理における畳み込みニューラルネットワークを理解する」で引用されている論文(hang, Y., & Wallace, B. (2015). A Sensitivity Analysis of (and Practitioners' Guide to) Convolutional Neural Networks for Sentence Classification)の、文章をカケラも読まずに図だけを見て作ったモノです。深層学習は道具立てが簡単なので、けっこう楽ちんに小難しい論文の内容を活用できちゃうんですよ。皆様も試してみてください。
正しくは、深層学習は「その適用方法が定まった分野をやる場合は」とても簡単なだけですけど。再帰型ニューラル・ネットワークの最先端とかは、やたらと難しいです……。↩
本稿の後半で紹介するTensorFlowの機能面でのメリットとして、自動微分があります。TensorFlowなら、計算グラフを作るだけで、学習に必要となる微分のコードを書かなくてもよいんですよ。私のような数学大嫌いな微分なにそれおいしいのという人間でも、最新の深層学習の計算グラフをただただ書き写せば動いてしまうというわけ。まぁ、Chainerでも、深層学習で一般的に使われる演算についてはライブラリ中に微分のコードが実装されていますから、通常は微分のことなんか考えなくて済みますけど。↩
nVidiaのCUDAに対応したGPUを積んだコンピューターでCUDAとcuDNNがセットアップされている場合は、
pip install tensorflow-gpuでGPUを使用して高速に動作するバージョンのTensorFlowをセットアップできます。GPU版を使用しても、本稿のコードは全て動作しますのでご安心を(Ubuntu 16.04 + GeForce 980 Tiで動作確認済み)。↩Chainerは、微分をライブラリ内に実装することでこの問題を解決しています。私のような一般人が作るニューラル・ネットワークなら、TensorFlowと同様に微分と無関係に深層学習できますので、ご安心ください。↩
遺伝的プログラミングのように、計算式そのものを生成するような学習もありますけど。↩
深層学習で使うニューラル・ネットワークってのは、微分(劣微分を含む)可能な演算でつながれた計算式にすぎません。だから、生物の脳やニューロンとは関係がないニューラル・ネットワークもあります。最近の再帰ニューラル・ネットワークなんかだと、ニューラルな感じはゼロで、なんでそれで答えを導けるのか全くわからなくて面白いですよ。↩
再帰型ニューラル・ネットワークについての説明は、ごめんなさい、使い方が定まるまで待ってください……。↩
数学的に理解している人たちも、結構行き当たりばったりでチューニングしているみたいだしね。↩
少なくとも私は、本稿に書いたレベルまでしか理解できていません。でも今までだいじょーぶでした。↩